#pragma once

#include <algorithm>
#include <initializer_list>
#include <limits>
#include <memory>
#include <sstream>
#include <functional>
#include <stdexcept>
#include <tuple>
#include <type_traits>
#include <vector>
#include <variant>

namespace NahidaProject {
	namespace NahidaMockTest {
		namespace MockDetails{
#if __cplusplus <= 201703L
			template <class T> struct __declspec(dllexport) RemoveCvref {
				using Type = typename std::remove_cv<typename std::remove_reference<T>::type>::type;
			};
#else
			using std::remove_cvref;
#endif

#if __cplusplus < 201703L
			template <class R, class Fn, class Arg>
			struct __declspec(dllexport) IsInvocableR : std::is_constructible<std::function<R(Arg)>, std::reference_wrapper<typename std::remove_reference<Fn>::type>>{

			};
#else
			using std::is_invocable_r;
#endif

			template <std::size_t... I> struct __declspec(dllexport) IndexSequence{ using type = IndexSequence; };
			template <size_t N> struct __declspec(dllexport) MakeIndexSequence{};

			template<> struct __declspec(dllexport) MakeIndexSequence<0> : IndexSequence<> {};
			template<> struct __declspec(dllexport) MakeIndexSequence<1> : IndexSequence<0> {};
			template<> struct __declspec(dllexport) MakeIndexSequence<2> : IndexSequence<0, 1> {};
			template<> struct __declspec(dllexport) MakeIndexSequence<3> : IndexSequence<0, 1, 2> {};
			template<> struct __declspec(dllexport) MakeIndexSequence<4> : IndexSequence<0, 1, 2, 3> {};
			template<> struct __declspec(dllexport) MakeIndexSequence<5> : IndexSequence<0, 1, 2, 3, 4> {};
			template<> struct __declspec(dllexport) MakeIndexSequence<6> : IndexSequence<0, 1, 2, 3, 4, 5> {};
			template<> struct __declspec(dllexport) MakeIndexSequence<7> : IndexSequence<0, 1, 2, 3, 4, 5, 6> {};
			template<> struct __declspec(dllexport) MakeIndexSequence<8> : IndexSequence<0, 1, 2, 3, 4, 5, 6, 7> {};
			template<> struct __declspec(dllexport) MakeIndexSequence<9> : IndexSequence<0, 1, 2, 3, 4, 5, 6, 7, 8> {};
			template<> struct __declspec(dllexport) MakeIndexSequence<10> : IndexSequence<0, 1, 2, 3, 4, 5, 6, 7, 8, 9> {};
		}

		namespace Invoking{
			using namespace NahidaProject::NahidaMockTest::MockDetails;

			template <class T> class __declspec(dllexport) ReturnValues{
			public:
				using StorageType = std::vector<T>;
				template <class... Args> T operator()(Args&&...){
					if (index == values.size()){
						return values.back();
					}
					else{
						return values[index++];
					}
				}

				StorageType values;
				std::size_t index;
			};

			class __declspec(dllexport) IgnoreArguments {
			public:
				explicit IgnoreArguments(const void*){

				}

				template <class T> void Save(const T&){

				}
			};

			template <class T> class __declspec(dllexport) SaveArguments {
				explicit SaveArguments(T* arg) : _arg(arg){

				}

				void Save(const T& value){
					*_arg = value;
				}

				T* _arg;
			};

			template <class T, typename = void> struct __declspec(dllexport) SyncArgumentsSave { using type = IgnoreArguments; };
			template <class T> struct __declspec(dllexport) SyncArgumentsSave<T&, typename std::enable_if<!std::is_const<T>::value>::type> { using type = SaveArguments<T>; };

			template <std::size_t I, class Args, class ArgSetters>
			class __declspec(dllexport) SyncArguments{
			private:
				Args* _args;
				ArgSetters* _setters;

			public:
				using ElementType = typename std::tuple_element<I, Args>::type;

				SyncArguments(Args& args, ArgSetters& setters) : _args(&args), _setters(&setters){

				}

				~SyncArguments(){
					std::get<I>(*_setters).Save(Get());
				}

				ElementType& Get(){
					return std::get<I>(*_args);
				}
			
			};

			template <class R, class F, class Args, class ArgSetters, std::size_t... I> static R InvokeIndexes(const F& func, Args& args, ArgSetters& setters, IndexSequence<I...>){
				return func(SyncArguments<I, Args, ArgSetters>(args, setters).Get()...);
			}

			template <class R, class F, class Args, class ArgSetters> static R Invoke(F&& func, Args& args, ArgSetters& setters){
				return InvokeIndexes<R>(func, args, setters, typename MakeIndexSequence<std::tuple_size<Args>::value>::type{});
			}
		}

		namespace Matching{
			using namespace NahidaProject::NahidaMockTest::MockDetails;
			class AnyMatcher {
				template <class T> bool operator()(const T&) const {
					return true; 
				}
			};

			template <class T> class __declspec(dllexport) EqualsMatcher{
			private:
				T _expected;
			public:
				EqualsMatcher(T&& value) : _expected(std::move(value)){

				}

				EqualsMatcher(const T& value) : _expected(value){

				}

				bool operator()(const T& actual) const{
					return actual == _expected;
				}
			
			};

			template <class Tuple> struct __declspec(dllexport) MethodArgumentsMatcher { virtual bool Matches(const Tuple& args) = 0; };

			template <class Tuple> struct __declspec(dllexport) AnyMethodArgumentsMatcher : MethodArgumentsMatcher<Tuple>{
				bool Matches(const Tuple&) override{
					return true;
				}
			};

			template <class Tuple, class MatcherTuple> class __declspec(dllexport) MatchArgumentsWrapper : public MethodArgumentsMatcher<Tuple>{
			private:
				MatcherTuple _predicates;
			public:
				explicit MatchArgumentsWrapper(MatcherTuple&& predicates) : _predicates(std::move(predicates)){

				}

				bool Matches(const Tuple& args) override{
					return MatchArguments(_predicates, args);
				}
			
			};

			static bool AndTogether(bool value){
				return value;
			}

			template <class Head, class... Tail> static bool AndTogether(Head head, Tail... tail){
				return head && AndTogether(tail...);
			}

			template <class P, class T, std::size_t... I> static bool MatchArgumentIndexes(const P& predicates, const T& args, IndexSequence<I...>){
				return AndTogether(std::get<I>(predicates)(std::get<I>(args))...);
			}

			template <class P, class T> static bool MatchArguments(const P& predicates, const T& args){
				static_assert(std::tuple_size<P>::value == std::tuple_size<T>::value, "The number of predicates must match the number of arguments");
				return MatchArgumentIndexes(predicates, args, typename MakeIndexSequence<std::tuple_size<T>::value>::type{});
			}
		}

		template <class R, class... Args>
		struct __declspec(dllexport) MethodAction{
			std::shared_ptr<Matching::MethodArgumentsMatcher<std::tuple<Args...>>> matcher;std::function<R(Args& ...)> action;
		};

		static Matching::AnyMatcher _;

		namespace Mocking{
			using namespace MockDetails;

			template <class Fn, class Arg> struct __declspec(dllexport) ArgumentMatcherWrapper : std::conditional<IsInvocableR<bool, Fn, Arg>::value, Fn, Matching::EqualsMatcher<Arg>>{};
			template <class... TupleArgs> struct __declspec(dllexport) ArgumentWrapper{ template <class... MatchArgs> struct MatchWith{ using type = std::tuple<typename ArgumentMatcherWrapper<MatchArgs, TupleArgs>::type...>;};};

			template <class R, class... Args> class __declspec(dllexport) MethodActionBuilder{
			public:
				explicit MethodActionBuilder(MethodAction<R, Args...>* action) : _action(action){

				}

				MethodActionBuilder(const MethodActionBuilder&) = delete;
				MethodActionBuilder(MethodActionBuilder&&) = default;

				~MethodActionBuilder(){
					if (_action->action == nullptr){
						if (_values.empty()){
							_values.push_back(R());
						}

						_action->action = Invoking::ReturnValues<R>{ std::move(_values), 0u };
					}

					if (_action->matcher == nullptr){
						_action->matcher = std::make_shared<Matching::AnyMethodArgumentsMatcher<std::tuple<Args...>>>();
					}
				}

				MethodActionBuilder& operator=(const MethodActionBuilder&) = delete;
				MethodActionBuilder& operator=(MethodActionBuilder&&) = default;

				template <class... MatchArgs> MethodActionBuilder& With(MatchArgs&&... args){
					static_assert( sizeof...(MatchArgs) == sizeof...(Args), "The number of matchers must match the number of arguments");

					using matcher_tuple = typename ArgumentWrapper<Args...>::template match_with<MatchArgs...>::type;
					_action->matcher = std::make_shared<Matching::MatchArgumentsWrapper<std::tuple<Args...>, matcher_tuple>>(matcher_tuple(std::forward<MatchArgs>(args)...));

					return *this;
				}

				MethodActionBuilder& Do(std::function<R(Args& ...)> function)
				{
					_action->action = std::move(function);
					return *this;
				}

				MethodActionBuilder& Return(const R& value)
				{
					_values.push_back(value);
					return *this;
				}

				MethodActionBuilder& Return(std::initializer_list<R> values)
				{
					for (auto& value : values)
					{
						_values.push_back(value);
					}

					return *this;
				}

				template <class E, class... ConstructorArgs>
				MethodActionBuilder& Throw(ConstructorArgs&&... args)
				{
					E exception(std::forward<ConstructorArgs>(args)...);
					_action->action = [=](const Args& ...) -> R
						{
							throw exception;
						};

					return *this;
				}
			private:
				MethodAction<R, Args...>* _action;
				typename Invoking::ReturnValues<R>::StorageType _values;
			};

			template <class... Args>
			class __declspec(dllexport) MethodActionBuilder<void, Args...>
			{
			public:
				explicit MethodActionBuilder(MethodAction<void, Args...>* action) :
					_action(action)
				{
				}

				MethodActionBuilder(const MethodActionBuilder&) = delete;
				MethodActionBuilder(MethodActionBuilder&&) = default;

				~MethodActionBuilder()
				{
					if (_action->action == nullptr)
					{
						_action->action = [](const Args& ...) {};
					}

					if (_action->matcher == nullptr)
					{
						_action->matcher = std::make_shared<Matching::AnyMethodArgumentsMatcher<std::tuple<Args...>>>();
					}
				}

				MethodActionBuilder& operator=(const MethodActionBuilder&) = delete;
				MethodActionBuilder& operator=(MethodActionBuilder&&) = default;

				template <class... MatchArgs>
				MethodActionBuilder& With(MatchArgs&&... args)
				{
					static_assert(sizeof...(MatchArgs) == sizeof...(Args), "The number of matchers must match the number of arguments");
					using matcher_tuple = typename ArgumentWrapper<Args...>::template match_with<MatchArgs...>::type;
					_action->matcher = std::make_shared<Matching::MatchArgumentsWrapper<std::tuple<Args...>, matcher_tuple>>(matcher_tuple(std::forward<MatchArgs>(args)...));

					return *this;
				}

				MethodActionBuilder& Do(std::function<void(Args & ...)> function)
				{
					_action->action = std::move(function);
					return *this;
				}

				template <class E, class... ConstructorArgs>
				MethodActionBuilder& Throw(ConstructorArgs&&... args)
				{
					E exception(std::forward<ConstructorArgs>(args)...);
					_action->action = [=](const Args& ...) -> void
						{
							throw exception;
						};

					return *this;
				}
			private:
				MethodAction<void, Args...>* _action;
			};

			template <class... Args>
			class __declspec(dllexport) MethodVerifyBuilder
			{
			public:
				MethodVerifyBuilder(
					const char* method,
					const char* file,
					std::size_t line,
					const std::vector<std::tuple<Args...>>* calls
				) :
					_calls(calls),
					_count(std::numeric_limits<std::size_t>::max()),
					_matched_count(calls->size()),
					_method(method),
					_file(file),
					_line(line)
				{
				}

				MethodVerifyBuilder(const MethodVerifyBuilder&) = delete;
				MethodVerifyBuilder(MethodVerifyBuilder&&) = default;

				~MethodVerifyBuilder() noexcept(false)
				{
					if (_count == std::numeric_limits<std::size_t>::max())
					{
						if (_matched_count == 0)
						{
							std::ostringstream stream;
							WriteLocation(stream) << "Expecting a call to "
								<< _method << " but none were received.";
							throw std::runtime_error(stream.str());
						}
					}
					else if (_count != _matched_count)
					{
						std::ostringstream stream;
						WriteLocation(stream) << "Expecting a call to " << _method << ' ';
						WriteTimes(stream, _count) << ", but it was invoked ";
						WriteTimes(stream, _matched_count) << '.';
						throw std::runtime_error(stream.str());
					}
				}

				MethodVerifyBuilder& operator=(const MethodVerifyBuilder&) = delete;
				MethodVerifyBuilder& operator=(MethodVerifyBuilder&&) = default;

				template <class... MatchArgs>
				MethodVerifyBuilder& With(MatchArgs&&... matchers)
				{
					static_assert(
						sizeof...(MatchArgs) == sizeof...(Args),
						"The number of matchers must match the number of arguments");

					using ArgumentTuple = std::tuple<Args...>;
					using MatcherTuple = typename ArgumentWrapper<Args...>::template match_with<MatchArgs...>::type;

					MatcherTuple matcher(std::forward<MatchArgs>(matchers)...);
					_matched_count = std::count_if(
						_calls->begin(),
						_calls->end(),
						[&](const ArgumentTuple& args) { return MatchArguments(matcher, args); });

					return *this;
				}

				MethodVerifyBuilder& Times(std::size_t count)
				{
					_count = count;
					return *this;
				}
			private:
				std::ostream& WriteLocation(std::ostream& stream)
				{
					return stream << _file << ':' << _line << ' ';
				}

				std::ostream& WriteTimes(std::ostream& stream, std::size_t count)
				{
					return stream << count << " time" << ((count == 1) ? "" : "s");
				}

				const std::vector<std::tuple<Args...>>* _calls;
				std::size_t _count;
				std::size_t _matched_count;
				const char* _method;
				const char* _file;
				std::size_t _line;
			};

			template <class>class MockMethodTypes;

			template <class R, class... Args>
			class __declspec(dllexport) MockMethodTypes<R(Args...)>
			{
			public:
				using MethodActionT = MethodAction<R, typename RemoveCvref<Args>::Type...>;
				using TupleT = typename std::tuple<typename RemoveCvref<Args>::Type ...>;

				using Action_T = typename std::vector<MethodActionT>;
				using Record_T = typename std::vector<TupleT>;
			};

			template <class R, class... Args>
			static MethodActionBuilder<R, Args...> AddAction(std::vector<MethodAction<R, Args...>>& actions)
			{
				actions.emplace_back(MethodAction<R, Args...>());
				return MethodActionBuilder<R, Args...>(&actions.back());
			}

			template <class... Args>
			static MethodVerifyBuilder<Args...> CheckAction(
				const char* method,
				const char* file,
				std::size_t line,
				const std::vector<std::tuple<Args...>>& invocations)
			{
				return MethodVerifyBuilder<Args...>(method, file, line, &invocations);
			}

			template <class T>
			static T ReturnDefault()
			{
				return T();
			}
		}
	}
}

#define EXPAND_MACRO(macro, ...) macro

#define MAKE_FORWARD(arg) std::forward<decltype(arg)>(arg)
#define MAKE_SETTER(arg) NahidaProject::NahidaMockTest::Invoking::SyncArgumentsSave<decltype(arg)>::type(&arg)

#define MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NameArgs, Transform, ...) \
    Ret Name(NameArgs Args) Specs \
    { \
        Name ## _Invocations.emplace_back(std::make_tuple( \
            EXPAND_MACRO(Transform(MAKE_FORWARD, __VA_ARGS__)))); \
        auto& args = Name ## _Invocations.back(); \
        for (auto it = Name ## _Actions.rbegin(); it != Name ## _Actions.rend(); ++it) \
        { \
            if (it->matcher->Matches(args)) \
            { \
                auto setters = std::make_tuple( \
                    EXPAND_MACRO(Transform(MAKE_SETTER, __VA_ARGS__))); \
                return NahidaProject::NahidaMockTest::Invoking::Invoke<Ret>(it->action, args, setters); \
            } \
        } \
        return NahidaProject::NahidaMockTest::Mocking::ReturnDefault<Ret>(); \
    } \
    NahidaProject::NahidaMockTest::Mocking::MockMethodTypes<Ret Args>::Action_T Name ## _Actions; \
    mutable NahidaProject::NahidaMockTest::Mocking::MockMethodTypes<Ret Args>::Record_T Name ## _Invocations;




#define NAME_ARGS1(type) type a
#define NAME_ARGS2(type, ...) type b, EXPAND_MACRO(NAME_ARGS1(__VA_ARGS__))
#define NAME_ARGS3(type, ...) type c, EXPAND_MACRO(NAME_ARGS2(__VA_ARGS__))
#define NAME_ARGS4(type, ...) type d, EXPAND_MACRO(NAME_ARGS3(__VA_ARGS__))
#define NAME_ARGS5(type, ...) type e, EXPAND_MACRO(NAME_ARGS4(__VA_ARGS__))
#define NAME_ARGS6(type, ...) type f, EXPAND_MACRO(NAME_ARGS5(__VA_ARGS__))
#define NAME_ARGS7(type, ...) type g, EXPAND_MACRO(NAME_ARGS6(__VA_ARGS__))
#define NAME_ARGS8(type, ...) type h, EXPAND_MACRO(NAME_ARGS7(__VA_ARGS__))
#define NAME_ARGS9(type, ...) type i, EXPAND_MACRO(NAME_ARGS8(__VA_ARGS__))
#define NAME_ARGS10(type, ...) type j, EXPAND_MACRO(NAME_ARGS9(__VA_ARGS__))

#define TRANSFORM1(Call, x) Call(x)
#define TRANSFORM2(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM1(Call, __VA_ARGS__))
#define TRANSFORM3(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM2(Call, __VA_ARGS__))
#define TRANSFORM4(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM3(Call, __VA_ARGS__))
#define TRANSFORM5(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM4(Call, __VA_ARGS__))
#define TRANSFORM6(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM5(Call, __VA_ARGS__))
#define TRANSFORM7(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM6(Call, __VA_ARGS__))
#define TRANSFORM8(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM7(Call, __VA_ARGS__))
#define TRANSFORM9(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM8(Call, __VA_ARGS__))
#define TRANSFORM10(Call, x, ...) Call(x), EXPAND_MACRO(TRANSFORM9(Call, __VA_ARGS__))

// We need to handle () and (int) differently, however, when they get passed in
// via __VA_ARG__ then it looks like we received a single parameter for both
// cases. Use the fact that we're always appending an 'a' to the parameter to
// detect the difference between 'type a' and 'a'
#define CAT(a, b) a ## b
#define GET_SECOND_ARG(a, b, ...) b
#define DEFINE_EXISTS(...) EXPAND_MACRO(GET_SECOND_ARG(__VA_ARGS__, TRUE))
#define IS_TYPE_MISSING(x) DEFINE_EXISTS(CAT(TOKEN_IS_EMPTY_, x))
#define TOKEN_IS_EMPTY_a ignored, FALSE
#define GET_METHOD(method, suffix) CAT(method, suffix)
#define NOOP(...)
#define MOCK_METHOD_IS_SINGLE_FALSE(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NOOP, NOOP)
#define MOCK_METHOD_IS_SINGLE_TRUE(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS1, TRANSFORM1, a)
#define HANDLE_EMPTY_TYPE(...) GET_METHOD(MOCK_METHOD_IS_SINGLE_, IS_TYPE_MISSING(__VA_ARGS__ a))

#define MOCK_METHOD_1(Ret, Name, Args, Specs) HANDLE_EMPTY_TYPE Args (Ret, Name, Args, Specs)
#define MOCK_METHOD_2(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS2, TRANSFORM2, b, a)
#define MOCK_METHOD_3(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS3, TRANSFORM3, c, b, a)
#define MOCK_METHOD_4(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS4, TRANSFORM4, d, c, b, a)
#define MOCK_METHOD_5(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS5, TRANSFORM5, e, d, c, b, a)
#define MOCK_METHOD_6(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS6, TRANSFORM6, f, e, d, c, b, a)
#define MOCK_METHOD_7(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS7, TRANSFORM7, g, f, e, d, c, b, a)
#define MOCK_METHOD_8(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS8, TRANSFORM8, h, g, f, e, d, c, b, a)
#define MOCK_METHOD_9(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS9, TRANSFORM9, i, h, g, f, e, d, c, b, a)
#define MOCK_METHOD_10(Ret, Name, Args, Specs) MOCK_METHOD_IMPL(Ret, Name, Args, Specs, NAME_ARGS10, TRANSFORM10, j, i, h, g, f, e, d, c, b, a)

#define GET_NTH_ARG(a, b, c, d, e, f, g, h, i, j, N, ...) N
#define GET_MOCK_METHOD(...) EXPAND_MACRO(GET_NTH_ARG(__VA_ARGS__, MOCK_METHOD_10, MOCK_METHOD_9, MOCK_METHOD_8, MOCK_METHOD_7, MOCK_METHOD_6, MOCK_METHOD_5, MOCK_METHOD_4, MOCK_METHOD_3, MOCK_METHOD_2, MOCK_METHOD_1))

#define INVALID_METHOD(...) static_assert(false, "Invalid usage. Call with return type, name, argument types and, optionally, specifiers.");
#define MOCK_METHOD_SPEC(Ret, Name, Args, Spec) GET_MOCK_METHOD Args (Ret, Name, Args, Spec)
#define MOCK_METHOD(Ret, Name, Args) MOCK_METHOD_SPEC(Ret, Name, Args, override)

#define MockMethod(...) EXPAND_MACRO(EXPAND_MACRO(GET_NTH_ARG(__VA_ARGS__, INVALID_METHOD, INVALID_METHOD, INVALID_METHOD, INVALID_METHOD, INVALID_METHOD, INVALID_METHOD, MOCK_METHOD_SPEC, MOCK_METHOD, INVALID_METHOD, INVALID_METHOD))(__VA_ARGS__))
#define MockConstMethod(Ret, Name, Args) MockMethod(Ret, Name, Args, const override)
#define When(call_method) NahidaProject::NahidaMockTest::Mocking::AddAction(call_method ## _Actions)
#define Verify(call_method) NahidaProject::NahidaMockTest::Mocking::CheckAction(#call_method, __FILE__, __LINE__, call_method ## _Invocations)

